﻿# Author: Abraham Cheng (tomcat.cheng@gmail.com)
# CollectSPOMigrationLog.ps1
# 2017-1-13
# This script is used for executing SharePoint online migration and collecting logs
[Cmdletbinding()]
Param (
    [Parameter()] # SiteUrl of the search center,e.g. https://chengc.sharepoint.com/sites/TestMigration/
    [String] $targetWeb,

    [Parameter()] # Lib or list URL which content need to be migrated,e.g. MigrationLib, Shared Documents
    [String] $targetDocLib,

    [Parameter()] # Admin user name of o365, e.g. abc@chengc.onmicorsoft.com
    [String]$username,

    [Parameter()] # Password of the admin user
    [String]$password,

	[Parameter()] # source file path
    [String]$sourceFiles,

	[Parameter()] # Migration type (on-premises, file-share)
	[String]$migrationType,

    [Parameter()] # switch for whether use the suffixId
    [switch] $usePathsuffixId
)

Import-Module Microsoft.Online.SharePoint.PowerShell
Add-Type -assembly "system.io.compression.filesystem"

$suffixId = ""
if($usePathsuffixId)
{
    $suffixId = [System.Guid]::NewGuid()
    $suffixId = $suffixId.ToString() # provide an identity for individual run of this script
}

if([string]::IsNullOrEmpty($targetWeb))
{
	$targetWeb = Read-Host -Prompt 'Please input the SharePoint online site URL which host your target library or list, e.g. https://chengc.sharepoint.com/sites/TestMigration/'
}

if([string]::IsNullOrEmpty($targetDocLib))
{
	$targetDocLib = Read-Host -Prompt 'Please input the target library or list URL, e.g. MigrationLib, Shared Documents'
}

if([string]::IsNullOrEmpty($username))
{
	$username = Read-Host -Prompt 'Please input the SharePoint online site admin account, e.g. abc@chengc.onmicrosoft.com'
}


if([string]::IsNullOrEmpty($sourceFiles))
{
	$sourceFiles = Read-Host -Prompt 'Please input the source file/package path, e.g. E:\MigrationTool\MigrationDemo\LibMigration\SourceFile'
}

if([string]::IsNullOrEmpty($migrationType) -or ($migrationType -ne 'on-premises' -and $migrationType -ne 'file-share'))
{
	$migrationType = Read-Host -Prompt 'Please input the migration type (on-premises or file-share), e.g. on-premises'
}

$secstr = New-Object -TypeName System.Security.SecureString

if([string]::IsNullOrEmpty($password))
{
	$secstr = Read-Host -Prompt 'Please input the password of the SharePoint online admin user' -AsSecureString
}
else
{	
	$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
}

$creds = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr

##############################################################
# Function description: This function used for writing logs
# -level is the log level, different log level will be written to different files
# -message message need to be written into the log
#  Call example:  WriteLog -level 'warning' -message $logMessage
#############################################################
function WriteLog($level, $message)
{  
   $messageColor = "White"
   if($level -eq "Error")
   {
         $messageColor = "Red"
   }
   elseif($level -eq "Warning")
   {
        $messageColor = "Yellow"
   }  

   $message =  "$level : $message";
   write-host $message -ForegroundColor $messageColor   
   $message  >> "CollectSPOMigrationLog_$suffixId-$level.txt"
}

function TestSharePointOnlineSiteConnection($webUrl, $user, $password, $libraryTitle)
{
	try
           {			    
			    $index = $webUrl.indexOf('sharepoint.com') + "sharepoint.com".Length
			    $relativeUrl =  $webUrl.Substring($index)
			    if($relativeUrl.LastIndexOf("/") -ne ($relativeUrl.Length -1))
			    {
				   $relativeUrl = $relativeUrl + "/"
			    }
			    
			    $relativeLibraryRUL = $relativeUrl + $libraryTitle
			    $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($webUrl)
				$web = $ctx.Web  
				$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($user,$password)
				$ctx.Load($web)
				$ctx.ExecuteQuery()
				$spList = $web.GetList($relativeLibraryRUL) # $libraryTitle should be the library URL 
				$ctx.Load($spList)
				$ctx.ExecuteQuery()
				if($spList.Title -ne $null)
				{
					WriteLog -level "Message" -message "Connected to $webUrl succesfully"
					return $true
				}
				else
				{
					WriteLog -level "Error" -message "Failed to connect to $webUrl"
				}
		   }
            catch [System.Exception]
            {
				$errormessage = $_.Exception.ToString()
				WriteLog -level "Error" -message "failed to connect to $webUrl by error message $errormessage" 
			}

	    return $false
}

function CreateSourcePackage($sourceFiles,$targetWebUrl,$targetLibrary,$cred)
{   
    try
    {
        WriteLog -level "Message" -message "Start to create source package"
        $index = $targetWebUrl.indexOf('sharepoint.com') + "sharepoint.com".Length
        $adminSite = $targetWebUrl.Substring(0,$index)
        if($adminSite.Contains("-my.sharepoint.com"))
        {
            $adminSite = $adminSite.Replace("-my.sharepoint.com", "-admin.sharepoint.com")
        }
        else
        {
            $adminSite = $adminSite.Replace(".sharepoint.com", "-admin.sharepoint.com")
        }

        WriteLog -level "Message" -message "Start to connect $adminSite"
        Connect-SPOService -Url $adminSite  -credential $cred
        WriteLog -level "Message" -message "Connected $adminSite"

        if(Test-Path "sourcePackage$suffixId")
          {
             WriteLog -level "Message" -message "Start to remove the sourcePackage folder as it is already existing"
             Remove-Item "sourcePackage$suffixId" -Force -Recurse
             WriteLog -level "Message" -message "Removed the sourcePackage folder as it is already existing"
          }

        $sourcePackage = New-Item -ItemType directory -Path "sourcePackage$suffixId"  -Force  
        WriteLog -level "Message" -message "Created the sourcePackage folder"

        New-SPOMigrationPackage -SourceFilesPath $sourceFiles -OutputPackagePath $sourcePackage.FullName -TargetWebUrl $targetWebUrl -TargetDocumentLibraryPath $targetLibrary -IgnoreHidden -ReplaceInvalidCharacters
        WriteLog -level "Message" -message "Created the source package"
        return $sourcePackage.FullName
    }
    catch [System.Exception]
    {
				$errormessage = $_.Exception.ToString()
				WriteLog -level "Error" -message "failed to create the source package by error message $errormessage" 
	}

	 return [string]::Empty
}

function ConvertToSPOMigrationTargetPackage($sourceFiles, $sourcePackage, $creds, $targetWeb, $targetDocLib)
{
  try
    {
      WriteLog -level "Message" -message "Start to create target package"
      if(Test-Path "targetPackage$suffixId")
      {
         WriteLog -level "Message" -message "Start to remove the targetPackage folder as it is already existing"
         Remove-Item "targetPackage$suffixId" -Force -Recurse
         WriteLog -level "Message" -message "Removed the targetPackage folder as it is already existing"
      }

      $targetPackage = New-Item -ItemType directory -Path "targetPackage$suffixId"  -Force
      WriteLog -level "Message" -message "Created the targetPackage folder"
      $finalPackages = ConvertTo-SPOMigrationTargetedPackage -SourceFilesPath $sourceFiles -SourcePackagePath $sourcePackage -OutputPackagePath $targetPackage.FullName -Credentials $creds -TargetWebUrl $targetWeb -TargetDocumentLibraryPath $targetDocLib
      WriteLog -level "Message" -message "Created the target package"
      return $targetPackage.FullName
   }
    catch [System.Exception]
    {
		$errormessage = $_.Exception.ToString()
		WriteLog -level "Error" -message "failed to convert the SPO migration target package by error message $errormessage" 
	}

   return [string]::Empty
}

function GetReportQueue($reportQueueAccessToken)
{
     WriteLog -level "Message" -message "reportQueueAccessToken:$reportQueueAccessToken"
     $queueInfo = @{}
     $queueInfo.SasTokenForQueue = "?" + $reportQueueAccessToken.Split("?".ToCharArray())[1]
     WriteLog -level "Message" -message "SasTokenForQueue:" + $queueInfo.SasTokenForQueue
     $queueInfo.QueueName = $reportQueueAccessToken.Substring($reportQueueAccessToken.LastIndexOf("/") + 1).Replace($queueInfo.SasTokenForQueue, "")
     WriteLog -level "Message" -message "QueueName:" + $queueInfo.QueueName
     $queueInfo.QueueEndPoint = New-Object Uri($reportQueueAccessToken.Substring(0, $reportQueueAccessToken.LastIndexOf("/")) + "/")     
     WriteLog -level "Message" -message "QueueEndPoint:" + $queueInfo.QueueEndPoint 
     $credentialsForQueue = new-object Microsoft.WindowsAzure.Storage.Auth.StorageCredentials($queueInfo.SasTokenForQueue);
     $queueClient = new-object Microsoft.WindowsAzure.Storage.Queue.CloudQueueClient($queueInfo.QueueEndPoint, $credentialsForQueue )
     $reprotQueue = $queueClient.GetQueueReference($queueInfo.QueueName)

     WriteLog -level "Message" -message "QueueEndPoint:" + $queueInfo.QueueEndPoint 
     return $reprotQueue
}

function DecryptMessage($message, $decryptKey)
{
    $messageEntity = ConvertFrom-Json -InputObject $message
    $iv = [Convert]::FromBase64String($messageEntity.IV)
    $encrptedMessage = [Convert]::FromBase64String($messageEntity.Content)
    
    $rijndaelManaged = New-Object System.Security.Cryptography.RijndaelManaged
    $rijndaelManaged.Key = $decryptKey
    $rijndaelManaged.IV = $iv 
    $rijndaelManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC

    $memoryStream = New-Object System.IO.MemoryStream -ArgumentList @(,$encrptedMessage)
    $cryptoStream = New-Object System.Security.Cryptography.CryptoStream ($memoryStream,$rijndaelManaged.CreateDecryptor($decryptKey, $iv), [System.Security.Cryptography.CryptoStreamMode]::Read)                              
    $streamReader = New-Object System.IO.StreamReader($cryptoStream)
    $decrptMessage = $streamReader.ReadToEnd()

    $rijndaelManaged.Dispose()
    $memoryStream.Dispose()
    $cryptoStream.Dispose()
    $streamReader.Dispose()

    return $decrptMessage
}

function WaitForMigrationAndGetLogs($jobInfo)
{
   # wait for the job executing
   WriteLog -level 'Message' -message "wait for the job executing"
   [System.Threading.Thread]::Sleep([TimeSpan]::FromMinutes(2))


    #Start to read the log
    $message = $null #Microsoft.WindowsAzure.Storage.Queue.CloudQueueMessage
    $queueOperation = New-Object Microsoft.WindowsAzure.Storage.Queue.QueueRequestOptions
    $reprotQueue = GetReportQueue($jobInfo.ReportingQueueUri.AbsoluteUri.ToString())
	$retryCount = 0
	$maxRetryCount = 60
    do{
        $message = $reprotQueue.GetMessage([TimeSpan]::FromMinutes(30), $null, $null)
        if ($message -ne $null)
        {
			$retryCount = 0
            $decryptMessage = DecryptMessage -message $message.AsString -decryptKey $jobInfo.Encryption.EncryptionKey
            WriteLog -level "Message"  -message "$decryptMessage"

            $decryptMessageEntity = ConvertFrom-Json -InputObject $decryptMessage

            if($decryptMessageEntity.Event -eq "JobEnd")
            {
                break # Get job end message, break the read log loop
            }
        }
		else{
			$retryCount++
		}

		[System.Threading.Thread]::Sleep([TimeSpan]::FromSeconds((10)))
     
    }while($retryCount -lt $maxRetryCount)

	If($retryCount -gt $maxRetryCount)
	{
		WriteLog -level "Warning" -message "Get the message from the report queue time out, retried 60 times, 10 seconds for each time, you can check the migration Job status by running command Get-SPOMigrationJobStatus -TargetWebUrl <string> -Credentials <CredentialCmdletPipeBind> [-JobId <guid>] [-NoLogFile]"
	}
}

function ZipLogs($sourcePackage, $targetPackage)
{ 
    WriteLog -level "Message" -message "Start to collect log files"
    if(Test-Path "Log$suffixId")
          {
             WriteLog -level "Message" -message "Start to remove the Log folder as it is already existing"
             Remove-Item "log$suffixId" -Force -Recurse
             WriteLog -level "Message" -message "Removed the Log folder as it is already existing"
          }

     $logDir = New-Item -ItemType directory -Path "Log$suffixId"  -Force
     if(Test-Path "Log$suffixId\SourcePackage")
     {
         Remove-Item "Log$suffixId\SourcePackage" -Force -Recurs
     }

     $sourcePackageLogDir = New-Item -ItemType directory -Path "Log$suffixId\SourcePackage"  -Force

     if(Test-Path "Log$suffixId\TargetPackage")
     {
        Remove-Item "Log$suffixId\TargetPackage" -Force -Recurse
     }

     $targetPackageLogDir = New-Item -ItemType directory -Path "Log$suffixId\TargetPackage"  -Force
     $metaDataFileNames = @('ExportSettings.xml',"LookupListMap.xml","Manifest*.xml","Requirements.xml","RootObjectMap.xml","SystemData.xml","UserGroup.xml","ViewFormsList.xml")

     foreach ($metaDataFile in $metaDataFileNames)
     {
          Copy-Item "$sourcePackage\$metaDataFile"  $sourcePackageLogDir.FullName
          Copy-Item "$targetPackage\$metaDataFile"   $targetPackageLogDir.FullName
     }

     Copy-Item "$sourcePackage\*.log" $logDir.FullName -Recurse      
     Copy-Item "$targetPackage\*.log" $logDir.FullName -Recurse 
     Copy-Item "$targetPackage\*\*.log" $logDir.FullName -Recurse 
     Copy-Item "CollectSPOMigrationLog_*.txt" $logDir.FullName -Recurse
     $currentLocation = (Get-Location).Path
     $zipfileName = $currentLocation +  "MigrationLog$suffixId.zip"

     if(Test-Path $zipfileName)
     {
         Remove-Item $zipfileName
     }

     [Io.Compression.ZipFile]::CreateFromDirectory($logDir.FullName, $zipfileName)     
     WriteLog -level "Message" -message "Logs has been ziped to $zipfileName"
 }

function Main()
{
        # Test SharePoint online site connection 
        $testConnectionResult = TestSharePointOnlineSiteConnection -webUrl $targetWeb -user $username -password $secstr -libraryTitle $targetDocLib
        if($testConnectionResult -eq $false)
        {
            return
        }

        # Check the migration type
        if($migrationType -ne 'on-premises' -and $migrationType -ne 'file-share')
        {
           WriteLog -level "Error" -message "The parameter migrationType:$migrationType is invalid, it only can be on-premises or file-share"
           return
        }

        # Checked the source path
        if((Test-Path -Path “$sourceFiles”) -eq $false)
        {
            WriteLog -level "Error" -message "Can't find the sourceFiles path $sourceFiles"
            return
        }

        # Create source package 
        $sourcePackage = $sourceFiles # If the migrationType is 'on-premises', then the source file path is the same as the source package path
        if($migrationType -eq 'file-share')
        {            
            $sourcePackage = CreateSourcePackage -sourceFiles $sourceFiles -targetWebUrl $targetWeb -targetLibrary $targetDocLib -cred $creds
            If([string]::IsNullOrEmpty($sourcePackage))
            {
               return
            }
        }

        # Convert source package to SPO migration target package         
       $targetPackage = ConvertToSPOMigrationTargetPackage -sourceFiles $sourceFiles -sourcePackage $sourcePackage -creds $creds -targetWeb $targetWeb -targetDocLib $targetDocLib
       if([string]::IsNullOrEmpty($targetPackage))
       {
           return
       }

       # Upload target package and submit job
       $job = Invoke-SPOMigrationEncryptUploadSubmit -SourceFilesPath $sourceFiles -SourcePackagePath $targetPackage  -Credentials $creds -TargetWebUrl $targetWeb 
       $jobId = $job.jobId
       WriteLog -level "Message"  -message "Migration　Job ID is $jobId"

       # Wait for Migration and get log
       WaitForMigrationAndGetLogs -jobInfo $job

       # Zip logs
       ZipLogs -sourcePackage $sourcePackage -targetPackage $targetPackage

       Write-Host "Done" -ForegroundColor Green
}

Main # Call main function